package birdsong.comm;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import javax.swing.JOptionPane;

import birdsong.comm.ClientSideMessage.Type;

/**
 * 
 * Communicator class for a Birdsong server. A Birdsong server must communicate
 * clients exclusively through this class. Implements the ServerCom.
 * 
 */
public class ServerCommunicator implements ServerComm {

        private final int SERVER_PORT = 8080;
        private HashMap<String, DealWithClient> mapDealWithClients = new HashMap<String, DealWithClient>();
        private BlockingQueue<ServerMessage> serverMessageListReceived = new LinkedBlockingQueue<ServerMessage>();
        private ServerCommunicator server = this;
        private ServerSocket serverSocket = null;

        /**
         * Server {@link Thread} that takes care of new clients. When a new
         * connection is established it opens a new {@link DealWithClient}.
         */
        @Override
        public void start() { 
                new Thread(new Runnable() {
                        @Override
                        public void run() {
                                try {
                                        setServerSocket(new ServerSocket(SERVER_PORT));
                                        while (true) {
                                                Socket socket = getServerSocket().accept();
                                                DealWithClient temp = new DealWithClient(socket, server);
                                                temp.start();
                                        }
                                } catch (IOException e) {
                                        JOptionPane
                                                        .showMessageDialog(
                                                                        null,
                                                                        "ERROR: one instance of the server is already running. \nSystem is shutting down.");
                                }

                        }

                }).start();
        }

        /**
         * Get the next message received from one of the clients. If no message is
         * has been received, the method blocks until a message has been received.
         * 
         * @return The next message sent by one of the connected clients to the
         *         server
         */
        @Override
        public ServerSideMessage getNextMessage() {
                ServerSideMessage serverMessageReceived = null;
                try {
                        serverMessageReceived = getServerMessageListReceived().take(); 
                } catch (InterruptedException e) {
                }
                return serverMessageReceived;
        }

        /**
         * Checks if a message from a client is pending. If
         * {@link #hasNextMessage()} returns <b>true</b>, a call to
         * {@link #getNextMessage()} will return immediately with the oldest message
         * in the queue. If {@link #hasNextMessage()} returns <b>false</b>, a call
         * to {@link #getNextMessage()} will block until a message has been
         * received.
         * 
         * @return <b>true</b> if a message from a client is currently pending,
         *         otherwise <b>false</b>. It will use the serverMessageListReceived
         *         to check if it is empty or not.
         */
        @Override
        public boolean hasNextMessage() {
                return !serverMessageListReceived.isEmpty();
        }

        /**
         * 
         * This method allows a thread to send a message to a target client. First
         * will search for that client and if he exists will allocate the message in
         * his DealWithClient list. If the target client doesn't exist it will not
         * send a message.
         * 
         * A Birdsong message to be forwarded to all followers. Payload = Birdsong
         * message
         * 
         * @param fromNickname
         *            - Name of sender.
         * @param toNickname
         *            - Name to send to.
         * @param birdSongMessage
         *            - Text to send.
         */
        @Override
        public void sendBirdsongMessage(String fromNickname, String toNickname,
                        String birdsongMessage) {
                BlockingQueue<ClientMessage> clientQueue = getListToSendToAClient(toNickname);
                if (clientQueue != null) {
                        clientQueue.add(new ClientMessage(Type.BIRDSONG_MESSAGE,
                                        birdsongMessage, fromNickname));
                }
        }

        private BlockingQueue<ClientMessage> getListToSendToAClient(
                        String toNickname) {
                BlockingQueue<ClientMessage> tempQueue = null;
                Iterator<Entry<String, DealWithClient>> i = getMapDealWithClients()
                                .entrySet().iterator();
                while (i.hasNext()) {
                        Entry<String, DealWithClient> temp = i.next();
                        if (temp.getValue().getClientNickname().equals(toNickname)) {
                                tempQueue = temp.getValue().getServerSenderMessageList();
                                break;
                        }
                }
                return tempQueue;
        }

        /**
         * Inform a client that another client is now a follower. If toNickname does
         * not exist, it will not send a message.
         * 
         * @param toNickname
         *            nickname of the client who has a new follower
         * @param newFollower
         *            nickname of the new follower
         */
        @Override
        public void sendNewFollower(String toNickname, String newFollower) {
                BlockingQueue<ClientMessage> clientQueue = getListToSendToAClient(toNickname);
                if (clientQueue != null) {
                        clientQueue.add(new ClientMessage(Type.FOLLOWER, newFollower,
                                        ClientSideMessage.SERVER));
                }
        }

        /**
         * Inform a client that a former follower has decided to unfollow. If
         * toNickname does not exist, it will not send a message.
         * 
         * @param toNickname
         *            nickname of the client who has lost a follower
         * @param unfollower
         *            nickname of the unfollower
         */
        @Override
        public void sendNewUnfollow(String toNickname, String unfollower) {
                BlockingQueue<ClientMessage> clientQueue = getListToSendToAClient(toNickname);
                if (clientQueue != null) {
                        clientQueue.add(new ClientMessage(Type.UNFOLLOWER, unfollower,
                                        ClientSideMessage.SERVER));
                }
        }

        /**
         * Report an error to a client. An error can, for instance, be trying to
         * follow a client who is not connected, trying to unfollow a client not
         * currently followed, and so on. If toNickname does not exist, it will not
         * send a message.
         * 
         * @param toNickname
         *            nickname of the client.
         * @param error
         *            description of the error.
         */
        @Override
        public void sendError(String toNickname, String error) {
                BlockingQueue<ClientMessage> clientQueue = getListToSendToAClient(toNickname);
                if (clientQueue != null) {
                        clientQueue.add(new ClientMessage(Type.ERROR, error,
                                        ClientSideMessage.SERVER));
                }
        }

        /**
         * Inform a client that a new client has connected. When a new client
         * connects, the server should call this method for each client already
         * connected to inform the newly connected client about the clients
         * currently online. If toNickname does not exist, it will not send a
         * message.
         * 
         * @param toNickname
         *            nickname of the client to whom to send the message.
         * @param connectedNickname
         *            nickname of the connected client.
         */
        @Override
        public void sendClientConnected(String toNickname, String connectedNickname) {
                BlockingQueue<ClientMessage> clientQueue = getListToSendToAClient(toNickname);
                if (clientQueue != null) {
                        clientQueue.add(new ClientMessage(Type.CONNECTED,
                                        connectedNickname, ClientSideMessage.SERVER));
                }
        }

        /**
         * Inform a client that another client disconnected. If toNickname does not
         * exist, it will not send a message.
         * 
         * @param toNickname
         *            nickname of the client to whom to send the message.
         * @param disconnectedNickname
         *            nickname of client who disconnected.
         */
        @Override
        public void sendClientDisconnected(String toNickname,
                        String disconnectedNickname) {
                BlockingQueue<ClientMessage> clientQueue = getListToSendToAClient(toNickname);
                if (clientQueue != null) {
                        clientQueue.add(new ClientMessage(Type.DISCONNECTED, disconnectedNickname, ClientSideMessage.SERVER));
                }
        }

        /**
         * Checks if a client with a certain nickname is currently connected. It
         * will search if the connection is active, if yes it will true, otherwise
         * will return false.
         * 
         * @param nickname
         *            nickname of the client to check.
         * @return <b>true</b> if a client with the nickname is currently connected,
         *         otherwise <b>false</b>.
         */
        @Override
        public boolean clientIsConnected(String nickname) {
                return getMapDealWithClients().containsKey(nickname);
        }

        /**
         * Disconnect a client. If nickname does exist, it will not remove it.
         * 
         * @param nickname
         *            nickname of the client to disconnect.
         */
        @Override
        public void disconnectClient(String nickname) {
                DealWithClient client = getTargetealWithClient(nickname);
                if (client != null) {
                        for (Thread clientThreads : client.getListThreadsActive()) {
                                clientThreads.interrupt();
                        }
                        try {
                                client.getSocket().close(); 
                        } catch (IOException e) {
                                e.printStackTrace(); 
                        }
                        getMapDealWithClients().remove(nickname);
                } 
        }

        private DealWithClient getTargetealWithClient(String nickname) {
                DealWithClient temp = null;
                Iterator<Entry<String, DealWithClient>> s = getMapDealWithClients().entrySet().iterator();
                while (s.hasNext()) {
                        Entry<String, DealWithClient> tempI = s.next();
                        if (tempI.getValue().getClientNickname().equals(nickname)) {
                                temp = tempI.getValue();
                                break;
                        }
                }
                return temp;
        }

        public synchronized HashMap<String, DealWithClient> getMapDealWithClients() {
                return mapDealWithClients;
        }

        public BlockingQueue<ServerMessage> getServerMessageListReceived() {
                return serverMessageListReceived;
        }

        public void setServerSocket(ServerSocket serverSocket) {
                this.serverSocket = serverSocket;
        }

        public ServerSocket getServerSocket() {
                return serverSocket;
        }

}

